/**
 *  Copyright Matt Silbernagel 2010
 *  
 *	This file is part of MobilePrinting.
 *
 *	MobilePrinting is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  MobilePrinting is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with MobilePrinting.  If not, see <http://www.gnu.org/licenses/>.
 */

package edu.uc.labs.MobilePrinting.PrintUtils;

import java.awt.TrayIcon;
import java.awt.TrayIcon.MessageType;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.xml.parsers.ParserConfigurationException;
import static edu.uc.labs.MobilePrinting.Configurator.*;
import edu.uc.labs.MobilePrinting.Fetch.PPDFetcher;
import edu.uc.labs.MobilePrinting.GUI.IconAnimator;
import edu.uc.labs.MobilePrinting.GUI.SysTray;

/**
 * 
 * @author Matt Silbernagel
 */
public class PrinterManager extends Thread{

	private String unc;
	private Printer printer = null;
	
	public PrinterManager(){
		printer = new Printer();
		unc = null;
	}

	public PrinterManager(Printer prn) {
		printer = prn;
		unc = prn.getDriver();
	}

	public void run() {
		// Change the icon to the progress icon
		IconAnimator progress = new IconAnimator();
		progress.start();
		//SysTray.setProgIcon();
		SysTray.sendMessage("Installing","Please wait while the printer, " + printer.getName() + ", is being installed", TrayIcon.MessageType.NONE);
		if (OS.matches("Windows(.)*")) {
			// If we are on a windows machine we need the
			// printer port added/updated
			SysTray.sendMessage("Installing", "Updating printer port...", TrayIcon.MessageType.NONE);
			boolean uPort = addPort();
			if (!uPort) {
				/*
				 * If the port update fails, let the user know 
				 * by sending a status message and changing the icon
				 * TODO: Put a status message in the drop down
				 */
				LOGGER.error("Unable to add/update port");
				progress.finish(false);
				SysTray.sendMessage("Error", "Unable to update the Printer Port", MessageType.ERROR);
				return;
			}
		} else if ( OS.startsWith("Mac") || OS.startsWith("Linux") ) {
			// If we are on a linux or Mac system we need a ppd file
			boolean check = checkForPPD();
			if (!check) {
				LOGGER.error("Could not find or create the PPD file\n\t\t " + printer.getPPD());
				SysTray.sendMessage("Error", "Unable to retrieve the correct PPD file", MessageType.ERROR);
				progress.finish(false);
				return;
			} else {
				LOGGER.debug("The ppd - " + printer.getPPD() + " - exists... ");
			}
		}
				
		LOGGER.debug("The printer is now being installed..." );
		SysTray.sendMessage("Installing", "Now installing and checking Drivers", TrayIcon.MessageType.NONE);
		boolean res = add(PRINTERNAME) ? true : false;
		if(res){
			LOGGER.debug("The printer should have been installed now checking");
			if(isPrinterInstalled( PRINTERNAME )){
				LOGGER.debug("it is!");
				try {
					// Write the cache file to the cache directory
					String cache_filename = CACHE + DELIM + printer.getZone() + ".xml";
					PrinterWriter.writeCache(printer, cache_filename);
					
					// Also write to a file named current.xml that keeps track
					// of the currently installed printer
					String current_filename = USERSPACE + CURRENT_PRINTER;
					PrinterWriter.writeCache(printer, current_filename);
				} catch (ParserConfigurationException e) {
					LOGGER.error("Encountered an error when trying to write the installed printer to a cache file: " + e.toString());
				} catch (IOException e) {
					LOGGER.error("PrinterManager: Encountered an IOException when trying to writeCache: " + e.toString());
				}
				SysTray.sendMessage("Success!", "Your printer is installed and ready to use", MessageType.NONE);
				LOGGER.debug("Trying to change the icon...");
				progress.finish(true);
				return;
			} else {
				/*
				 * If the install failed let the user know by 
				 * setting the error icon and sending a status message
				 */
				LOGGER.debug("It isn't :(");
			}
		}
		LOGGER.error("The printer didn't get installed for some reason");
		progress.finish(false);
		SysTray.sendMessage("Failed", "Encountered and error while installing your printer", MessageType.ERROR);
		return;
	}

	/**
	 * Method that will rename a printer by removing the current printer
	 * and re-adding it with the new name
	 * @param oldName what the current printer is called
	 * @param newName what the new name will be
	 * @throws IOException 
	 */
	public void renamePrinter(String oldName, String newName) {
		try {
			if(remove(oldName)){
				// The printer was removed, now try
				// to install it under it's new name
				if( add(newName) ){
					// Successfully added the new printer
					LOGGER.debug("Successfully renamed the printer");
				} else {
					// Unable to install the printer 
				}
			} else {
				LOGGER.error("Unable to rename the printer, because couldn't remove it");
			}
		} catch (IOException e) {
			LOGGER.error("Unable to rename the printer");
		}
			
	}
	
	/**
	 * This method will remove the printer based on the name
	 * passed in
	 * 
	 * @param pName
	 *            The name of the printer to remove
	 * @return true if the printer was removed succesfully false otherwise
	 */
	public boolean remove(String pName) throws IOException {
		ArrayList<String> cmd = new ArrayList<String>();
		String checkString = null;
		boolean isRemoved = false;
		boolean isInstalled = isPrinterInstalled( pName );
		String line = null;
		@SuppressWarnings("unused")
		String error = null;
		
		if (!isInstalled){
			// If the printer isn't installed
			// there is no need to remove it
			return true;
		}
		
		if (OS.startsWith("Win")) {
			cmd.add("cscript.exe");
			cmd.add(WINSCRIPTSPATH + "\\prnmngr.vbs");
			cmd.add("-d");
			cmd.add("-p");
			cmd.add(pName);
			checkString = "Deleted printer (.)*";
		} else if(OS.startsWith("Mac") || OS.startsWith("Linux")) {
			cmd.add("lpadmin");
			cmd.add("-x");
			cmd.add(pName);
			checkString = "";
		}
			
		// Convert ArrayList to Array
		//String command[] = (String[]) cmd.toArray();
		String[] command = listToArray(cmd);	
		
		LOGGER.debug("Removing printer " + pName);
		LOGGER.debug("Using the command " + PrinterManager.arrayToString(command, " "));
		Process p = Runtime.getRuntime().exec(command);	
		
		// Get the output and error streams
		BufferedReader output = new BufferedReader(new InputStreamReader( p.getInputStream()));
		BufferedReader errput = new BufferedReader(new InputStreamReader( p.getErrorStream()));	
		
		LOGGER.debug("Here is the standard output of the remove printer command:\n");			 
		while ((line = output.readLine()) != null) {		
			LOGGER.debug(line);	
			Pattern pattern = Pattern.compile(checkString);	
			Matcher mat = pattern.matcher(line);	
			if(mat.find()) {
				isRemoved = true;
			}
		}
		if (output.readLine() == null) { isRemoved = true; }
		
		LOGGER.debug("Here is the standard error of the remove printer command:\n");
		while ((error = errput.readLine()) != null) {
			LOGGER.error(line);
			isRemoved = false;
		}				
		return isRemoved;
	}

	/**
	 * Windows needs a port to associate with a printer
	 * this method will add/update that port 
	 * @return
	 * @throws IOException
	 */
	public boolean addPort() {
		String line = null;
		String[] addPortcmd = {"cscript.exe ", WINSCRIPTSPATH + "\\prnport.vbs", "-a", "-r", PORTNAME, "-h", printer.getServer(), "-q", printer.getQueue(), "-o",  "lpr" };

		LOGGER.debug("Updating port... ");
		Process p = null;
		try {
			p = Runtime.getRuntime().exec(addPortcmd);
		} catch (IOException e) {
			LOGGER.error("PrinterManager#addPort: Unable to run the command - " + arrayToString(addPortcmd, " ") + "e.toString");
		}
		boolean successful = false;
		// What does the output of the command report
		BufferedReader input = new BufferedReader(new InputStreamReader(p
				.getInputStream()));
		try {
			while ((line = input.readLine()) != null) {
				LOGGER.debug(line);
				Pattern pattern = Pattern.compile("Created/updated port (.)*");
				Matcher mat = pattern.matcher(line);
				if (mat.find()) {
					successful = true;
				}
			}
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		return successful;
	}

	/**
	 * This method will install the print named printerName
	 * @param printerName What the printer will be named when installed
	 * @return true if the printer was successfully added
	 * @throws IOException
	 */
	@SuppressWarnings({ "unused" })
	private boolean add(String printerName) {
		
		String line = null;
		String error = null;
		String rValue = null;
		String eValue = null;
		String checkString = null;
		boolean isAdded = false;
		ArrayList<String> cmd = new ArrayList<String>();
		
		if(OS.startsWith("Win") && !isPrinterInstalled(printerName)){
			LOGGER.debug("This is windows and the printer isn't already installed");
			// Only install on windows if it currently isn't installed
			cmd.add("rundll32");
			cmd.add("printui.dll,PrintUIEntry");
			cmd.add("/if");
			cmd.add("/b");
			cmd.add(printerName);
			cmd.add("/f");
			cmd.add(unc);
			cmd.add("/u");
			cmd.add("/r");
			cmd.add(PORTNAME);
			cmd.add("/m");
			cmd.add("HP Universal Printing PS");
		} else if(OS.startsWith("Mac") || OS.startsWith("Linux")) {
			LOGGER.debug("PrinterManager: It appears you are using Mac OS X or Linux, installing the printer now...");
			cmd.add("/usr/sbin/lpadmin");
			cmd.add("-p");
			cmd.add(PRINTERNAME);
			cmd.add("-o");
			cmd.add("printer-is-shared=false");
			cmd.add("-v");
			cmd.add("lpd://" + printer.getServer() + "/" + printer.getQueue());
			cmd.add("-L");
			cmd.add(printer.getLocation());
			cmd.add("-D");
			cmd.add(PRINTERNAME);
			cmd.add("-P");
			cmd.add(printer.getPPD());
			cmd.add("-E");		
		}else if(OS.startsWith("Win") && isPrinterInstalled(printerName)){
			LOGGER.debug("It appears that you are using windows and the printer is already installed.");
			isAdded = true;
			return isAdded;
		}else{
			LOGGER.error("Your OS isn't supported: " + OS );
			return isAdded;
		}
		
		// Convert the ArrayList to an Array
		String[] command = listToArray(cmd);
		
		LOGGER.debug("The command to run for installing the printer: " + PrinterManager.arrayToString(command, " "));
		//String[] command = (String[]) cmd.toArray();
		
		try{
			LOGGER.debug("Installing the printer...");
			Process p = Runtime.getRuntime().exec(command);
			
			// Get the output and error streams
			BufferedReader output = new BufferedReader(new InputStreamReader(p
					.getInputStream()));
			BufferedReader errput = new BufferedReader(new InputStreamReader(p
					.getErrorStream()));
			
			// Read the output
			while ((line = output.readLine()) != null) {
				rValue += line;
			}
			
			// Read the errors
			while ((error = output.readLine()) != null){
				eValue += line;
			}
		}catch (IOException e){
			LOGGER.error("Error installing printer: " + e.toString());
			return isAdded;
		}
		
		if (eValue != null){
			LOGGER.error("Command returned an error: \n\t" + eValue );
			return isAdded;
		}
		
		if (rValue != null){
			LOGGER.debug("The install command returned something on the standard output: \n\t" +
					rValue + "\n\n");
		}
		
		LOGGER.debug("We didn't get any errors!");
		// We didn't get an error lets test to see if the printer got installed
		for (int i = 0; i < 2; i++) {
			LOGGER.debug("printerName = " + printerName);
			if (isPrinterInstalled(printerName)) {
				isAdded = true;
				break;
			} else {
				LOGGER.debug("We didn't get an error but the printer didn't " +
							 "report that it was installed, will check again in 2 seconds");
				try {
					Thread.sleep(2000);
				} catch (InterruptedException e) {
					LOGGER.debug("Caught an exception while trying to sleep");
				}
			}
		}
		return isAdded;
	}

	/**
	 * This method checks for a printer installed on the system by name
	 * @param printerName - the printer name to search for
	 * @return true if that printer is installed
	 */

	private boolean isPrinterInstalled( String printerName ) {
		boolean isInstalled = false;
		ArrayList<String> cmd = new ArrayList<String>();
		String checkString = null;	
		String line = null;
		
		if (OS.startsWith("Win")){
			cmd.add("cscript.exe");
			cmd.add(WINSCRIPTSPATH + "\\prnmngr.vbs");
			cmd.add("-l");
			checkString = "Printer name " + printerName + "(.)*";
		} else if(OS.startsWith("Mac") || OS.startsWith("Linux")) {
			cmd.add("lpstat");
			cmd.add("-a");
			checkString = printerName + "\\s(.)*";
		} else {
			LOGGER.error("Operating system not supported: " + OS);
			return isInstalled;
		}
		
		// Convert the ArrayString to an Array
		// Convert the ArrayList to an Array
		String[] command = listToArray(cmd);
		
		LOGGER.debug("Checking for existence of " + printerName + "...");
		LOGGER.debug("Running this command: " + PrinterManager.arrayToString(command, " "));
		
		try {
			Process p = Runtime.getRuntime().exec(command);

			// Get the output and error streams
			BufferedReader output = new BufferedReader(new InputStreamReader(p
					.getInputStream()));
			@SuppressWarnings("unused")
			BufferedReader errput = new BufferedReader(new InputStreamReader(p
					.getErrorStream()));

			// Read the output
			while ((line = output.readLine()) != null) {
				Pattern pattern = Pattern.compile(checkString);
				Matcher mat = pattern.matcher(line);
				if (mat.find()) {
					System.out.println("There is printer installed called "
							+ printerName);
					isInstalled = true;
				}
			}
		} catch (IOException e) {
			LOGGER.error("Recieved an error while searching for the printer " + printerName );
			LOGGER.error(e.toString());
			return isInstalled;
		}
		return isInstalled;
	}

	/**
	 * Method that checks for the proper ppd
	 * Fetch's it if nesseccary
	 * @return true if ppd is available
	 */
	private boolean checkForPPD() {
		boolean doesExist = false;
		File f = new File(printer.getPPD());
		for (int i = 0; i < 4; i++) {
			if (f.exists()) {
				// The ppd already exists, start the installation
				LOGGER.debug("The ppd already exists...");
				doesExist = true;
				break;
			} else {
				LOGGER.debug("The ppd does not exist yet...");
				PPDFetcher fetch = new PPDFetcher(HTTP_SERVER, printer
						.getZone(), printer.getQueue(), OS);
				fetch.writeToFile();
			}
		}
		return doesExist;
	}

	/**
	 * Method to display an Array in String format
	 * @param a the Array
	 * @param separator what array fields be seperated by
	 * 		  this will usually be a space (" ")
	 * @return
	 */
	public static String arrayToString(String[] a, String separator) {
		String result = "";
		if (a.length > 0) {
			result = a[0]; // start with the first element
			for (int i = 1; i < a.length; i++) {
				result = result + separator + a[i];
			}
		}
		return result;
	}


	private String[] listToArray(ArrayList<String> cmd){
		// Convert the ArrayList to an Array
		int size = cmd.size();
		String[] command = new String[size];
		for(int i=0; i<size; i++){
			command[i] = cmd.get(i);
		}
		return command;
	}
}
